package net.pimpas.framework;

import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import java.util.Map.Entry;

import net.pimpas.configuration.PimpasFrameworkConfiguration;
import net.pimpas.context.DefaultFrameworkContext;
import net.pimpas.core.configuration.exception.ConfigurationException;
import net.pimpas.core.configuration.file.FileConfiguration;
import net.pimpas.core.context.FrameworkContext;
import net.pimpas.core.scanner.Scanner;
import net.pimpas.core.serviceprovider.ServiceProvider;
import net.pimpas.core.serviceprovider.ServiceProviderAnnotation;
import net.pimpas.core.serviceprovider.instancetype.ServiceProviderInstanceType;
import net.pimpas.core.visitor.Visitor;
import net.pimpas.exception.FrameworkException;
import net.pimpas.exception.PimpasFrameworkException;
import net.pimpas.scanner.ClassPathScanner;
import net.pimpas.scanner.PimpasFrameworkScanner;
import net.pimpas.util.reflection.ReflectionUtil;
import net.pimpas.visitor.BaseVisitorClass;

import org.apache.log4j.Logger;

public class PimpasFramework implements Framework {
	private static Logger logger = Logger.getLogger(PimpasFramework.class);

	private Map<Class<? extends ServiceProvider>, Class<? extends ServiceProvider>> servicesProvider;
	private Map<Class<? extends ServiceProvider>, ServiceProvider> singletons;
	private boolean configured;
	private List<Visitor> visitors;
	private FrameworkContext context;
	private Thread cleanupThread;
	private int cleanupInterval;

	private static Framework pimpas; 
	
	private PimpasFramework() {
		this.setServicesProvider(new HashMap<Class<? extends ServiceProvider>, Class<? extends ServiceProvider>>());
		this.setSingletons(new HashMap<Class<? extends ServiceProvider>, ServiceProvider>());
		this.setVisitors(new ArrayList<Visitor>());
		this.setConfigured(false);
	}
	
	public static Framework getCurrentInstance() {
		if(PimpasFramework.pimpas == null) {
			synchronized(PimpasFramework.class) {
				// still null?!
				if(PimpasFramework.pimpas == null) {
					PimpasFramework.pimpas = new PimpasFramework();
					try {
						PimpasFramework.pimpas.config();
					} catch (FrameworkException e) {
						System.out.println("Fatal Exception when trying to config Framework!");
						throw new RuntimeException(e);
					}
				}
			}
		}
		
		return PimpasFramework.pimpas;
	}

	public void registerServiceProvider(Class<? extends ServiceProvider> serviceProviderType, Class<? extends ServiceProvider> serviceProvider) {
		if(!this.getServicesProvider().containsKey(serviceProviderType)) {
			// check interface
			if(net.pimpas.util.reflection.ReflectionUtil.isSubclass(serviceProviderType, ServiceProvider.class) &&
					net.pimpas.util.reflection.ReflectionUtil.isSubclass(serviceProvider, serviceProviderType)) {
				// check annotation
				if(net.pimpas.util.reflection.ReflectionUtil.checkAnnotationInClass(ServiceProviderAnnotation.class, serviceProvider)) {
					logger.debug("Adding ServiceProvider => Type: "+serviceProviderType.getName()+", Provider: "+serviceProvider.getName());
					this.getServicesProvider().put(serviceProviderType, serviceProvider);
				} else
					logger.error("Cannot add serviceProvider: "+serviceProvider.getName()+", Reason: class need hav annotation ServiceProviderTypeAnnotation");
			} else
				logger.error("Cannnot add serviceType: "+serviceProviderType.getName()+", Reason: wrong classes type!");
		} else
			logger.error("Cannot replace serviceProvider for type: "+serviceProviderType.getName());
	}

	private Map<Class<? extends ServiceProvider>, Class<? extends ServiceProvider>> getServicesProvider() {
		return servicesProvider;
	}

	private void setServicesProvider(Map<Class<? extends ServiceProvider>, Class<? extends ServiceProvider>> serviceProviders) {
		this.servicesProvider = serviceProviders;
	}

	@SuppressWarnings("unchecked")
	public void config() throws FrameworkException {
		// get configuration
		if(this.isConfigured()) {
			logger.error("PimpasFramework was configured before!");
			return;
		}
		long startedConfig = System.currentTimeMillis();
		Scanner pimpasScan = new PimpasFrameworkScanner();
		pimpasScan.scan();
		
		List<FileConfiguration> files = pimpasScan.getResult();
		PimpasFrameworkConfiguration pimpasConfig = new PimpasFrameworkConfiguration();
		pimpasConfig.setPimpas(this);
		pimpasConfig.getFilesConfiguration().addAll(files);
		
		try {
			pimpasConfig.consume();
		} catch (ConfigurationException e) {
			throw new PimpasFrameworkException("Unable to config Framework", e);
		}
		
		this.setConfigured(true);
		this.setCleanupInterval(pimpasConfig.getCleanup());
		if(this.getCleanupInterval() == -1) {
			logger.info("[PimpasFramework]: Cleanup thread disabled!");
		} else {
			if(this.getCleanupInterval() < 60000)
				this.setCleanupInterval(60000);
			logger.info("[PimpasFramework]: Setting cleanup interval to "+this.getCleanupInterval()+" milliseconds.");
		}
		
		if(!pimpasConfig.isSkipClasspathScanner()) {
			// run classpathscanner (getting visitors class)
	        // add baseVisitor Class
	        this.getVisitors().add(new BaseVisitorClass());
			ClassPathScanner classPath = new ClassPathScanner();
			classPath.scan();
			List<Class<?>> visitors = BaseVisitorClass.getVisitorClasses();
			this.getVisitors().clear();
			for(int i=0;i<visitors.size();i++)
				try {
					this.getVisitors().add((Visitor) visitors.get(i).newInstance());
				} catch (Exception e) {
					throw new PimpasFrameworkException("Unable to config Framework", e);
				}
				
			// run again classpath scanner
			classPath.scan();
			// clear visitors
			this.getVisitors().clear();
			this.setVisitors(null);
		} else {
			logger.warn("[PIMPASFRAMEWORK]: WARNING!!!!!! CLASSPATHSCANNER DISABLED!!!!");
			logger.warn("[PIMPASFRAMEWORK]: Use only for webstart Application!");
			logger.warn("[PIMPASFRAMEWORK]: To enable CLASSPATHSCANNER set the pimpas/config/scanner/alive to true !!!");
		}
		// check context, is null?!
		if(this.getContext() == null) {
			this.setContext(new DefaultFrameworkContext());
			logger.info("[PIMPASFRAMEWORK]: Context not found, using default context.");
		}
		
		// configure cleanup thread
		/*
		this.setCleanupThread(new Thread() {
			@Override
			public void run() {
				for(;;) {
					try {
						Thread.sleep(cleanupInterval);
						logger.debug("[PimpasFramework]: Running cleanup thread...");
						Iterator<Class<? extends ServiceProvider>> providers = 
							singletons.keySet().iterator();
						ServiceProvider provider = null;
						Stack<Class<? extends ServiceProvider>> terminateds =
							new Stack<Class<? extends ServiceProvider>>();
						Class<? extends ServiceProvider> clazz = null;
						while(providers.hasNext()) {
							clazz = providers.next();
							provider = singletons.get(clazz);
							if(provider.isTerminated()) {
								logger.debug("[PimpasFramework]: Removing ServiceProvider ["+provider+"] from singleton instances.");
								terminateds.push(clazz);
							}
						}
						while(!terminateds.isEmpty())
							synchronized(singletons) {
								singletons.remove(terminateds.pop());
							}
						logger.debug("[PimpasFramework]: End of cleanup!");
					} catch (InterruptedException e) {
					}
				}
			}
		});
		*/
		long finishedConfig = System.currentTimeMillis();
		Calendar c = Calendar.getInstance();
		c.setTimeInMillis((finishedConfig-startedConfig));
		logger.debug("[PIMPASFRAMEWORK] TOTAL CONFIGURATION TIME: ("+c.get(Calendar.MINUTE)+
				" minutes and "+c.get(Calendar.SECOND)+" seconds and "+
				c.get(Calendar.MILLISECOND)+" milliseconds).");
		logger.debug("[PIMPAS FRAMEWORK CONFIGURED]");
		/*
		if(this.getCleanupInterval() > 0)
			this.getCleanupThread().start();
		*/
	}

	@SuppressWarnings("unchecked")
	public ServiceProvider getServiceProvider(Class<? extends ServiceProvider> clazzType, Object... params)  {
		if(!net.pimpas.util.reflection.ReflectionUtil.isSubclass(clazzType, ServiceProvider.class) ||
				clazzType == ServiceProvider.class) {
			logger.error("provider type "+clazzType.getName()+" its not a subclass of "+ServiceProvider.class.getName());
			return null;
		}
		
		Set set = this.getServicesProvider().entrySet();
		Iterator it = set.iterator();
		Class tmp = null;
		Entry entry = null;
		ServiceProvider provider = null;
		Constructor constructor = null;
		Constructor[] constructors = null;
		Class parameter = null;
		boolean parametersNotMatch = false;
		ServiceProviderAnnotation ann = null;
		ServiceProviderInstanceType instanceType = null;
		serviceProviderFind:
		while(it.hasNext()) {
			entry = (Entry) it.next();
			tmp = (Class) entry.getKey();
			if(tmp.equals(clazzType)) {
				try {
					// check instance in singletons
					if(this.getSingletons().get(tmp) != null) {
						provider = this.getSingletons().get(tmp);
						logger.debug("Singleton class found in singleton instances, provider: "+provider.getClass().getSimpleName());
						return provider;
					}
					
					constructors = ((Class)entry.getValue()).getConstructors();
					if(constructors.length > 0) {
						if(params.length > 0) {
							// try instance using parameters
							for(int i=0;i<constructors.length;i++) {
								constructor = constructors[i];
								if(constructor.getParameterTypes().length == params.length) {
									// check parameters
									parametersNotMatch = false;
									parametersFind:
									for(int j=0;j<constructor.getParameterTypes().length;j++) {
										parameter = constructor.getParameterTypes()[j];
										if(!ReflectionUtil.isSubclass(params[j].getClass(), parameter))
											parametersNotMatch = true;
											break parametersFind;
									}
									if(!parametersNotMatch) {
										provider = (ServiceProvider) constructor.newInstance(params);
										break serviceProviderFind;
									}
								}
							}
						}
					} else {
						logger.error("Cannot find any constructor for "+entry);
						throw new RuntimeException("Cannot find any constructor for "+entry.getValue());
					}
					
					// instance default constructor (no-parameters)
					provider = (ServiceProvider) ((Class)entry.getValue()).newInstance();
					break;
				} catch (Exception e) {
					logger.fatal("Fatal exception when trying to get service provider... ", e);
					throw new RuntimeException(e);
				}
			}
		}
		
		try {
			if(provider != null) {
				provider.config();
				// check singleton class
				ann = (ServiceProviderAnnotation) provider.getClass().getAnnotation(ServiceProviderAnnotation.class);
				instanceType = ann.instanceType();
				if(instanceType.equals(ServiceProviderInstanceType.SINGLETON)) {
					logger.debug("Singleton Provider found, saving in singleton instances, provider: "+provider.getClass().getName());
					this.getSingletons().put(clazzType, provider);
				}
			} else
				logger.error("Cannot find provider for provider type: "+clazzType.getName());
		} catch (ConfigurationException e) {
			logger.fatal("Error while configuring provider ["+provider.getClass().getName()+"]", e);
			throw new RuntimeException(e);
		}
		
		return provider;
	}


	private boolean isConfigured() {
		return configured;
	}


	private void setConfigured(boolean configured) {
		this.configured = configured;
	}


	private Map<Class<? extends ServiceProvider>, ServiceProvider> getSingletons() {
		return singletons;
	}


	private void setSingletons(Map<Class<? extends ServiceProvider>, ServiceProvider> singletons) {
		this.singletons = singletons;
	}


	public List<Visitor> getVisitors() {
		return visitors;
	}


	public void setVisitors(List<Visitor> visitors) {
		this.visitors = visitors;
	}


	public FrameworkContext getContext() {
		return context;
	}


	public void setContext(FrameworkContext context) {
		this.context = context;
	}

	public void executeCleanUp() {
		
	}

	public Thread getCleanupThread() {
		return cleanupThread;
	}

	public void setCleanupThread(Thread cleanupThread) {
		this.cleanupThread = cleanupThread;
	}

	public int getCleanupInterval() {
		return cleanupInterval;
	}

	public void setCleanupInterval(int cleanupInterval) {
		this.cleanupInterval = cleanupInterval;
	}
}